<?php

namespace EsBuilder;

use Closure;
use Elasticsearch\ClientBuilder;

class EBuilder
{
    public $hosts = [];

    public $index;

    public $type;

    public $elasticsearch;

    /**
     * @var array
     */
    public $operators = [
        '=' => 'eq',
        '>' => 'gt',
        '>=' => 'gte',
        '<' => 'lt',
        '<=' => 'lte',
    ];

    public $allowOperators = [
        '>',
        '>=',
        '=',
        '<',
        '<=',
        '<>',
        '!=',
        'like',
        'not like'
    ];

    /**
     * @var EGrammar
     */
    public $grammar;

    public $config;

    public $offset;

    public $limit;

    public $orders;

    public $wheres;

    public $columns;

    public $enableQueryLog;

    public $queryLogs;

    public $highlight;

    public $aggs;

    public $groupBy;

    /**
     * 初始化
     * [
     *     'hosts' => [['host' => '10.3.7.66', 'port' => '9200'], [['host' => '10.3.7.55', 'port' => '9200']]]
     *     'index' => 'index_wpt_community_posts_test',
     *     'type' => 'type_posts',
     * ]
     * EBuilder constructor.
     * @param array $config
     */
    public function __construct(array $config)
    {
        $this->config = $config;
        $this->setConfig();
        $this->setGrammar();
        $this->setElasticSearch();
    }

    public function newQuery()
    {
        return new self($this->config);
    }

    protected function setConfig()
    {
        if (!empty($this->config['hosts'])) {
            $this->hosts = $this->config['hosts'];
        }

        if (!empty($this->config['index'])) {
            $this->index = $this->config['index'];
        }

        if (!empty($this->config['type'])) {
            $this->type = $this->config['type'];
        }
    }

    protected function setGrammar()
    {
        $this->grammar = new EGrammar();
    }

    protected function setElasticSearch()
    {
        $this->elasticsearch = ClientBuilder::create()->setHosts($this->hosts)->build();
    }

    public function getElasticSearch()
    {
        return $this->elasticsearch;
    }

    public function index(string $index)
    {
        $this->index = $index;
        return $this;
    }

    public function type(string $type)
    {
        $this->type = $type;
        return $this;
    }

    public function orderBy(string $column, $direction = 'asc')
    {
        $this->orders[$column] = $direction;
        return $this;
    }

    public function offset(int $offset)
    {
        $this->offset = $offset;
        return $this;
    }

    public function limit(int $limit)
    {
        $this->limit = $limit;
        return $this;
    }

    public function take(int $value)
    {
        return $this->limit($value);
    }

    public function skip(int $value)
    {
        return $this->offset($value);
    }

    /**
     * 高亮搜索结果
     * @param string $column 需要高亮的字段名称
     * @param array $params es高亮的额外配置
     * [
     *    "pre_tags" => "<b class='key' style='color:red'>", //高亮开始标签
     *    "post_tags" => "</b>", //高亮结束标签
     * ]
     * 更多参数支持查看: https://www.elastic.co/guide/en/elasticsearch/reference/6.8/search-request-highlighting.html
     * @return $this
     */
    public function highlight(string $column, array $params = [])
    {
        $this->highlight['fields'][$column] = $params ?: (object)$params;
        return $this;
    }

    public function groupBy(string $column, string $sortDirection = 'desc')
    {
        $this->limit = 0;

        $this->groupBy[] = compact('column', 'sortDirection');

        return $this;
    }

    /**
     * 使用原生es aggs语法
     * @param array $aggs
     * @return EBuilder
     */
    public function aggs(array $aggs)
    {
        $this->limit = 0;
        $this->groupBy = null;
        $this->aggs = $aggs;
        return $this;
    }

    /**
     * 设置要查询的字段
     * @param array $columns 要查询的字段
     * @return $this
     */
    public function select(array $columns)
    {
        $this->columns = $columns;
        return $this;
    }

    /**
     * 获取查询结果列表
     * @return \Illuminate\Support\Collection
     */
    public function get()
    {
        $result = $this->runQuery($this->grammar->compileQuery($this));

        return $this->formatResult($result);
    }

    /**
     * 获取第一条查询结果
     * @return mixed
     */
    public function first()
    {
        $this->limit = 1;
        $result = $this->runQuery($this->grammar->compileQuery($this));

        return $this->formatResult($result)->first();
    }

    /**
     * 统计查询数量
     * @return int|mixed
     */
    public function count()
    {
        $result = $this->runQuery($this->grammar->compileQuery($this), 'count');

        return $result['count'] ?? 0;
    }

    /**
     * 创建一条记录
     * @param array $data 数据
     * @param string $key 文档id要使用的字段名
     * @return bool
     * @throws \Exception
     */
    public function create(array $data, $key = 'id')
    {
        $id = isset($data[$key]) ? $data[$key] : '';
        if (empty($id)) {
            throw new \Exception('Create error, undefined index:id, params:' . json_encode($data));
        }

        $result = $this->runQuery($this->grammar->compileCreate($this, $id, $data), 'create');

        if (!isset($result['result']) || $result['result'] !== 'created') {
            throw new \Exception('Create error, params: ' . json_encode($this->getLastQueryLog()));
        }

        return true;
    }

    /**
     * 更新文档数据
     * @param int|string $id 文档id
     * @param array $data 数据
     * @return bool
     * @throws \Exception
     */
    public function update($id, array $data)
    {
        $result = $this->runQuery($this->grammar->compileUpdate($this, $id, $data), 'update');

        if (!isset($result['result']) || $result['result'] !== 'updated') {
            throw new \Exception('Update error params: ' . json_encode($this->getLastQueryLog()));
        }

        return true;
    }

    /**
     * 删除文档
     * @param int|string $id 文档id
     * @return bool
     * @throws \Exception
     */
    public function delete($id)
    {
        $result = $this->runQuery($this->grammar->compileDelete($this, $id), 'delete');

        if (!isset($result['result']) || $result['result'] !== 'deleted') {
            throw new \Exception('Delete error, params:' . json_encode($this->getLastQueryLog(), JSON_UNESCAPED_UNICODE));
        }

        return true;
    }

    /**
     * 获取最后一次执行的es查询结构
     * @return mixed|string
     */
    public function getLastQueryLog()
    {
        return !empty($this->queryLogs) ? end($this->queryLogs) : '';
    }

    /**
     * 开启查询日志
     * @return $this
     */
    public function enableQueryLog()
    {
        $this->enableQueryLog = true;
        return $this;
    }

    /**
     * 关闭查询日志
     * @return $this
     */
    public function disableQueryLog()
    {
        $this->enableQueryLog = false;
        return $this;
    }

    /**
     * 格式化查询结果
     * @param $result
     * @return \Illuminate\Support\Collection
     */
    protected function formatResult($result)
    {
        if (!empty($this->groupBy)) {
            $data =  $this->formatGroupByResult($result['aggregations']);
            return collect($data);
        } elseif (!empty($this->aggs)) {
            return collect($result['aggregations']);
        } else {
            return collect($result['hits']['hits'])->map(function ($hit) {
                if (!empty($this->highlight)) {
                    return array_merge($hit['_source'], $hit['highlight']);
                }
                return $hit['_source'];
            });
        }
    }

    protected function formatGroupByResult($aggregations)
    {
        $data = [];
        if (!empty($aggregations)) {
            foreach ($aggregations as $column => $aggregation) {
                foreach ($aggregation['buckets'] as $bucket) {
                    $key = array_pull($bucket, 'key');
                    $count = array_pull($bucket, 'doc_count');
                    $data[] = [
                        $column => $key,
                        'count' => $count,
                        'subData' => $bucket ? $this->formatGroupByResult($bucket) : []
                    ];
                }
            }
        }
        return $data;
    }

    /**
     * 执行ES查询
     * @param array $params
     * @param string $method
     * @return mixed
     */
    protected function runQuery(array $params, $method = 'search')
    {
        if ($this->enableQueryLog) {
            $this->queryLogs[] = $params;
        }

        return call_user_func([$this->elasticsearch, $method], $params);
    }

    public function where($column, $operator = null, $value = null, $boolean = 'and')
    {
        $type = 'Base';
        $leaf = 'term';

        if ($column instanceof \Closure) {
            return $this->whereNested($column, $boolean);
        }

        if (func_num_args() === 2 || !in_array($operator, $this->allowOperators)) {
            list($value, $operator) = [$operator, '='];
        }

        if (is_array($column)) {
            return $this->addArrayOfWheres($column, $boolean);
        }

        $operator = $operator && isset($this->operators[$operator]) ? $this->operators[$operator] : $operator;

        if (in_array($operator, ['gt', 'gte', 'lt', 'lte'])) {
            $leaf = 'range';
        } elseif ($operator == '<>' || $operator == '!=') {
            $type = 'Not';
        } elseif ($operator == 'like') {
            $leaf = 'match';
        } elseif ($operator == 'not like') {
            $type = 'Not';
            $leaf = 'match';
        }

        $this->wheres[] = compact('type', 'column', 'leaf', 'value', 'boolean', 'operator');

        return $this;
    }

    /**
     * @param $column
     * @param array $value
     * @param string $boolean
     * @return EBuilder
     */
    public function whereNotIn(string $column, array $value, $boolean = 'and')
    {
        $type = 'Not';
        $leaf = 'terms';
        $operator = 'not in';
        $this->wheres[] = compact('type', 'column', 'leaf', 'value', 'boolean', 'operator');

        return $this;
    }

    public function whereIn(string $column, array $value, $boolean = 'and')
    {
        $type = 'Base';
        $leaf = 'terms';
        $operator = 'in';
        $this->wheres[] = compact('type', 'column', 'leaf', 'value', 'boolean', 'operator');

        return $this;
    }

    public function orWhere($column, $operator = null, $value = null)
    {
        return $this->where($column, $operator, $value, 'or');
    }

    protected function whereNested(Closure $callback, $boolean)
    {
        $subQuery = $this->newQuery();

        call_user_func($callback, $subQuery);

        return $this->addNestedOfQuery($subQuery, $boolean);
    }

    /**
     * 回调函数处理
     * @param EBuilder $query
     * @param $boolean
     * @return $this
     */
    protected function addNestedOfQuery(EBuilder $query, $boolean)
    {
        if (count($query->wheres)) {
            $type = 'Nested';
            $this->wheres[] = compact('type', 'boolean', 'query');
        }
        return $this;
    }

    /**
     * 数组条件处理
     * @param $wheres
     * @param $boolean
     * @param string $method
     * @return $this
     */
    protected function addArrayOfWheres($wheres, $boolean, $method = 'where')
    {
        return $this->whereNested(function ($subQuery) use ($wheres, $method) {
            foreach ($wheres as $key => $value) {
                if (is_numeric($key) && is_array($value)) {
                    call_user_func_array([$subQuery, $method], $value);
                } else {
                    $subQuery->$method($key, '=', $value);
                }
            }
        }, $boolean);
    }
}
